﻿namespace NorthernLights.Encryption.Asymmetric
{
    using System;
    using System.Text;
    using RSA;

    /// <summary>
    /// The Asymetric Encryption class.
    /// </summary>
    public static class Encryption
    {
        #region Public methods
        /// <summary>
        /// Generate Key pair.
        /// </summary>
        /// <param name="completed">Executed when completed with new keypair or null.</param>
        public static void GenerateKeyAsync(Action<string> completed)
        {
            GenerateKeyAsync(2048, 65537, completed);
        }

        /// <summary>
        /// Generate Key pair.
        /// </summary>
        /// <param name="cipherStrength">The desired cipher strength.</param>
        /// <param name="completed">Executed when completed with new keypair or null.</param>
        public static void GenerateKeyAsync(int cipherStrength, Action<string> completed)
        {
            GenerateKeyAsync(cipherStrength, 65537, completed);
        }

        /// <summary>
        /// Generate Key pair.
        /// </summary>
        /// <param name="cipherStrength">The desired cipher strength</param>
        /// <param name="pubExponent">the desired pub exponent.</param>
        /// <param name="completed">Executed when completed with new keypair or null.</param>
        public static void GenerateKeyAsync(int cipherStrength, int pubExponent, Action<string> completed)
        {
            if (cipherStrength % 8 != 0)
            {
                throw new ArgumentException("Invalid cipherStrength");
            }

            if (pubExponent > 65537)
            {
                throw new ArgumentException("Invalid pubExponent");
            }

            try
            {
                RSACrypto crypto = new RSACrypto(cipherStrength);

                crypto.OnKeysGenerated += (sender) =>
                {
                    string result = crypto.ToXmlString(true);

                    if (completed != null)
                    {
                        completed(result);
                    }
                };

                crypto.GenerateKeys(cipherStrength, pubExponent);
            }
            catch (Exception ex)
            {
                DebugLogger.Instance.Write(ex);

                if (completed != null)
                {
                    completed(null);
                }
            }
        }

        /// <summary>
        /// Validates the input string as a valid RSA Xml private key.
        /// </summary>
        /// <param name="privateKey">The RSA Xml private key to validate.</param>
        /// <returns>True if valid.</returns>
        public static bool IsValidPrivateKey(string privateKey)
        {
            try
            {
                RSACrypto crypto = new RSACrypto();
                crypto.FromXmlString(privateKey);

                RSAParameters parameters = crypto.ExportParameters();

                if (parameters.D.Length != 0 && parameters.DP.Length != 0 &&
                    parameters.DQ.Length != 0 && parameters.IQ.Length != 0 &&
                    parameters.P.Length != 0 && parameters.Q.Length != 0)
                {
                    return true;
                }
            }
            catch (Exception ex)
            {
                DebugLogger.Instance.Write(ex);
            }

            return false;
        }

        /// <summary>
        /// Get public key from keypair.
        /// </summary>
        /// <param name="xmlKeys">The keypair.</param>
        /// <returns>The public key or null.</returns>
        public static string GetPublicKey(string xmlKeys)
        {
            if (string.IsNullOrEmpty(xmlKeys))
            {
                return null;
            }

            string result = null;

            try
            {
                RSACrypto crypto = new RSACrypto();
                crypto.FromXmlString(xmlKeys);
                result = crypto.ToXmlString(false);
            }
            catch (Exception ex)
            {
                DebugLogger.Instance.Write(ex);
            }

            return result;
        }

        /// <summary>
        /// Encrypts a message using a public key.
        /// </summary>
        /// <param name="publicKey">The public key to use.</param>
        /// <param name="message">The message to encrypt.</param>
        /// <returns>The encrypted message or null.</returns>
        public static byte[] Encrypt(string publicKey, byte[] message)
        {
            if (string.IsNullOrEmpty(publicKey))
            {
                return null;
            }

            RSACrypto crypto = new RSACrypto();
            crypto.FromXmlString(publicKey);

            return Encrypt(crypto, message);
        }

        /// <summary>
        /// Encrypts a message using a <see cref="RSACrypto"/> object.
        /// </summary>
        /// <param name="crypto">The <see cref="RSACrypto"/> object to use.</param>
        /// <param name="message">The message to encrypt.</param>
        /// <returns>The encrypted message or null.</returns>
        public static byte[] Encrypt(RSACrypto crypto, byte[] message)
        {
            if (crypto == null || message == null)
            {
                return null;
            }

            if (message.Length > crypto.MaxMessageLength - 1)
            {
                throw new ArgumentOutOfRangeException("message to long");
            }

            byte[] result = null;

            try
            {
                byte[] rawBytes = message;
                result = crypto.Encrypt(rawBytes);
            }
            catch (Exception ex)
            {
                DebugLogger.Instance.Write(ex);
            }

            return result;
        }

        /// <summary>
        /// Decrypts a message using a private key.
        /// </summary>
        /// <param name="privateKey">The private key to use.</param>
        /// <param name="encryptedMessage">The message to decrypt.</param>
        /// <returns>The decrypted message or null.</returns>
        public static byte[] Decrypt(string privateKey, byte[] encryptedMessage)
        {
            if (string.IsNullOrEmpty(privateKey) || !IsValidPrivateKey(privateKey))
            {
                return null;
            }

            RSACrypto crypto = new RSACrypto();
            crypto.FromXmlString(privateKey);

            return Decrypt(crypto, encryptedMessage);
        }

        /// <summary>
        /// Decrypts a message using a <see cref="RSACrypto"/> object.
        /// </summary>
        /// <param name="crypto">The <see cref="RSACrypto"/> object to use.</param>
        /// <param name="encryptedMessage">The message to decrypt.</param>
        /// <returns>The decrypted message or null.</returns>
        public static byte[] Decrypt(RSACrypto crypto, byte[] encryptedMessage)
        {
            if (crypto == null || encryptedMessage == null)
            {
                return null;
            }

            byte[] result = null;

            try
            {
                byte[] rawBytes = encryptedMessage;
                result = crypto.Decrypt(rawBytes);
            }
            catch (Exception ex)
            {
                DebugLogger.Instance.Write(ex);
            }

            return result;
        }

        /// <summary>
        /// Sign a message.
        /// </summary>
        /// <param name="privateKey">The private key to use for signing.</param>
        /// <param name="message">The message to sign.</param>
        /// <returns>The base64 encoded signature or null.</returns>
        public static string Sign(string privateKey, string message)
        {
            var a = UTF8Encoding.UTF8.GetBytes(message);
            var b = Sign(privateKey, a);

            return Convert.ToBase64String(b, 0, b.Length);
        }

        /// <summary>
        /// Sign a message.
        /// </summary>
        /// <param name="privateKey">The private key to use for signing.</param>
        /// <param name="message">The message to sign.</param>
        /// <returns>The signature or null.</returns>
        public static byte[] Sign(string privateKey, byte[] message)
        {
            if (string.IsNullOrEmpty(privateKey) || message == null)
            {
                return null;
            }

            byte[] result = null;

            try
            {
                RSACrypto crypto = new RSACrypto();
                crypto.FromXmlString(privateKey);

                RSA.SignatureProviders.EMSAPKCS1v1_5_SHA256 sigProvider = new RSA.SignatureProviders.EMSAPKCS1v1_5_SHA256();

                result = crypto.SignData(message, sigProvider);
            }
            catch (Exception ex)
            {
                DebugLogger.Instance.Write(ex);
            }

            return result;
        }

        /// <summary>
        /// Verify signature.
        /// </summary>
        /// <param name="publicKey">The publickey to use for validation.</param>
        /// <param name="signature">The signature to use.</param>
        /// <param name="unsignedData">The data to validate.</param>
        /// <returns>True if validated.</returns>
        public static bool VerifySignature(string publicKey, string base64encodedSignature, string unsignedData)
        {
            var a = Convert.FromBase64String(base64encodedSignature);
            var b = UTF8Encoding.UTF8.GetBytes(unsignedData);

            return VerifySignature(publicKey, a, b);
        }

        /// <summary>
        /// Verify signature.
        /// </summary>
        /// <param name="publicKey">The publickey to use for validation.</param>
        /// <param name="signature">The signature to use.</param>
        /// <param name="unsignedData">The data to validate.</param>
        /// <returns>True if validated.</returns>
        public static bool VerifySignature(string publicKey, byte[] signature, byte[] unsignedData)
        {
            if (string.IsNullOrEmpty(publicKey) || signature == null || unsignedData == null)
            {
                return false;
            }

            try
            {
                RSACrypto crypto = new RSACrypto();
                crypto.FromXmlString(publicKey);

                RSA.SignatureProviders.EMSAPKCS1v1_5_SHA256 sigProvider = new RSA.SignatureProviders.EMSAPKCS1v1_5_SHA256();
                
                return crypto.VerifyData(unsignedData, signature, sigProvider);
            }
            catch (Exception ex)
            {
                DebugLogger.Instance.Write(ex);
            }

            return false;
        }
        #endregion
    }
}
